 /*******************************************************************************
  * Copyright (c) 2000, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.ui;

 import java.lang.reflect.InvocationTargetException ;
 import java.lang.reflect.Method ;
 import java.util.ArrayList ;
 import java.util.Iterator ;
 import java.util.List ;

 import org.eclipse.core.runtime.IAdaptable;
 import org.eclipse.core.runtime.IConfigurationElement;
 import org.eclipse.core.runtime.Platform;
 import org.eclipse.jface.viewers.ISelection;
 import org.eclipse.jface.viewers.IStructuredSelection;
 import org.eclipse.jface.viewers.StructuredSelection;
 import org.eclipse.ui.actions.SimpleWildcardTester;
 import org.eclipse.ui.internal.ActionExpression;
 import org.eclipse.ui.internal.registry.IWorkbenchRegistryConstants;
 import org.eclipse.ui.internal.util.Util;
 import org.eclipse.ui.model.IWorkbenchAdapter;
 import org.osgi.framework.Bundle;

 /**
  * Determines the enablement status given a selection. This calculation is done
  * based on the definition of the <code>enablesFor</code> attribute,
  * <code>enablement</code> element, and the <code>selection</code> element
  * found in the <code>IConfigurationElement</code> provided.
  * <p>
  * This class can be instantiated by clients. It is not intended to be extended.
  * </p>
  *
  * @since 3.0
  *
  * Note: The dependency on org.eclipse.jface.text for ITextSelection must be
  * severed It may be possible to do with IActionFilter generic workbench
  * registers IActionFilter for "size" property against IStructuredSelection
  * workbench text registers IActionFilter for "size" property against
  * ITextSelection code here: sel.getAdapter(IActionFilter.class) As an interim
  * solution, use reflection to access selections implementing ITextSelection
  */
 public final class SelectionEnabler {

     /* package */static class SelectionClass {
         public String className;

         public String nameFilter;

         public boolean recursive;
     }

     public static final int ANY_NUMBER = -2;

     /**
      * The constant integer hash code value meaning the hash code has not yet
      * been computed.
      */
     private static final int HASH_CODE_NOT_COMPUTED = -1;

     /**
      * A factor for computing the hash code for all schemes.
      */
     private static final int HASH_FACTOR = 89;

     /**
      * The seed for the hash code for all schemes.
      */
     private static final int HASH_INITIAL = SelectionEnabler.class.getName()
             .hashCode();

     /**
      * Cached value of <code>org.eclipse.jface.text.ITextSelection.class</code>;
      * <code>null</code> if not initialized or not present.
      */
     private static Class iTextSelectionClass = null;

     /**
      * Hard-wired id of the JFace text plug-in (not on pre-req chain).
      */
     private static final String JFACE_TEXT_PLUG_IN = "org.eclipse.jface.text"; //$NON-NLS-1$

     public static final int MULTIPLE = -5;

     public static final int NONE = -4;

     public static final int NONE_OR_ONE = -3;

     public static final int ONE_OR_MORE = -1;

     /**
      * Hard-wired fully qualified name of the text selection class (not on
      * pre-req chain).
      */
     private static final String TEXT_SELECTION_CLASS = "org.eclipse.jface.text.ITextSelection"; //$NON-NLS-1$

     /**
      * Indicates whether the JFace text plug-in is even around. Without the
      * JFace text plug-in, text selections are moot.
      */
     private static boolean textSelectionPossible = true;

     public static final int UNKNOWN = 0;

     /**
      * Returns <code>ITextSelection.class</code> or <code>null</code> if the
      * class is not available.
      *
      * @return <code>ITextSelection.class</code> or <code>null</code> if
      * class not available
      * @since 3.0
      */
     private static Class getTextSelectionClass() {
         if (iTextSelectionClass != null) {
             // tried before and succeeded
 return iTextSelectionClass;
         }
         if (!textSelectionPossible) {
             // tried before and failed
 return null;
         }

         // JFace text plug-in is not on prereq chain of generic wb plug-in
 // hence: ITextSelection.class won't compile
 // and Class.forName("org.eclipse.jface.text.ITextSelection") won't find
 // it need to be trickier...
 Bundle bundle = Platform.getBundle(JFACE_TEXT_PLUG_IN);
         if (bundle == null || bundle.getState() == Bundle.UNINSTALLED) {
             // JFace text plug-in is not around, or has already
 // been removed, assume that it will never be around
 textSelectionPossible = false;
             return null;
         }

         // plug-in is around
 // it's not our job to activate the plug-in
 if (bundle.getState() == Bundle.INSTALLED) {
             // assume it might come alive later
 textSelectionPossible = true;
             return null;
         }

         try {
             Class c = bundle.loadClass(TEXT_SELECTION_CLASS);
             // remember for next time
 iTextSelectionClass = c;
             return iTextSelectionClass;
         } catch (ClassNotFoundException e) {
             // unable to load ITextSelection - sounds pretty serious
 // treat as if JFace text plug-in were unavailable
 textSelectionPossible = false;
             return null;
         }
     }

     /**
      * Verifies that the given name matches the given wildcard filter. Returns
      * true if it does.
      *
      * @param name
      * @param filter
      * @return <code>true</code> if there is a match
      */
     public static boolean verifyNameMatch(String name, String filter) {
         return SimpleWildcardTester.testWildcardIgnoreCase(filter, name);
     }

     private List classes = new ArrayList ();

     private ActionExpression enablementExpression;

     /**
      * The hash code for this object. This value is computed lazily, and marked
      * as invalid when one of the values on which it is based changes.
      */
     private transient int hashCode = HASH_CODE_NOT_COMPUTED;

     private int mode = UNKNOWN;

     /**
      * Create a new instance of the receiver.
      *
      * @param configElement
      */
     public SelectionEnabler(IConfigurationElement configElement) {
         super();
         if (configElement == null) {
             throw new IllegalArgumentException ();
         }
         parseClasses(configElement);
     }

     public final boolean equals(final Object object) {
         if (object instanceof SelectionEnabler) {
             final SelectionEnabler that = (SelectionEnabler) object;
             return Util.equals(this.classes, that.classes)
                     && Util.equals(this.enablementExpression,
                             that.enablementExpression)
                     && Util.equals(this.mode, that.mode);
         }

         return false;
     }

     /**
      * Computes the hash code for this object based on the id.
      *
      * @return The hash code for this object.
      */
     public final int hashCode() {
         if (hashCode == HASH_CODE_NOT_COMPUTED) {
             hashCode = HASH_INITIAL * HASH_FACTOR + Util.hashCode(classes);
             hashCode = hashCode * HASH_FACTOR
                     + Util.hashCode(enablementExpression);
             hashCode = hashCode * HASH_FACTOR + Util.hashCode(mode);
             if (hashCode == HASH_CODE_NOT_COMPUTED) {
                 hashCode++;
             }
         }
         return hashCode;
     }

     /**
      * Returns true if given structured selection matches the conditions
      * specified in the registry for this action.
      */
     private boolean isEnabledFor(ISelection sel) {
         Object obj = sel;
         int count = sel.isEmpty() ? 0 : 1;

         if (verifySelectionCount(count) == false) {
             return false;
         }

         // Compare selection to enablement expression.
 if (enablementExpression != null) {
             return enablementExpression.isEnabledFor(obj);
         }

         // Compare selection to class requirements.
 if (classes.isEmpty()) {
             return true;
         }
         if (obj instanceof IAdaptable) {
             IAdaptable element = (IAdaptable) obj;
             if (verifyElement(element) == false) {
                 return false;
             }
         } else {
             return false;
         }

         return true;
     }

     /**
      * Returns true if given text selection matches the conditions specified in
      * the registry for this action.
      */
     private boolean isEnabledFor(ISelection sel, int count) {
         if (verifySelectionCount(count) == false) {
             return false;
         }

         // Compare selection to enablement expression.
 if (enablementExpression != null) {
             return enablementExpression.isEnabledFor(sel);
         }

         // Compare selection to class requirements.
 if (classes.isEmpty()) {
             return true;
         }
         for (int i = 0; i < classes.size(); i++) {
             SelectionClass sc = (SelectionClass) classes.get(i);
             if (verifyClass(sel, sc.className)) {
                 return true;
             }
         }
         return false;
     }

     /**
      * Returns true if given structured selection matches the conditions
      * specified in the registry for this action.
      */
     private boolean isEnabledFor(IStructuredSelection ssel) {
         int count = ssel.size();

         if (verifySelectionCount(count) == false) {
             return false;
         }

         // Compare selection to enablement expression.
 if (enablementExpression != null) {
             return enablementExpression.isEnabledFor(ssel);
         }

         // Compare selection to class requirements.
 if (classes.isEmpty()) {
             return true;
         }
         for (Iterator elements = ssel.iterator(); elements.hasNext();) {
             Object obj = elements.next();
             if (obj instanceof IAdaptable) {
                 IAdaptable element = (IAdaptable) obj;
                 if (verifyElement(element) == false) {
                     return false;
                 }
             } else {
                 return false;
             }
         }

         return true;
     }

     /**
      * Check if the receiver is enabled for the given selection.
      *
      * @param selection
      * @return <code>true</code> if the given selection matches the conditions
      * specified in <code>IConfirgurationElement</code>.
      */
     public boolean isEnabledForSelection(ISelection selection) {
         // Optimize it.
 if (mode == UNKNOWN) {
             return false;
         }

         // Handle undefined selections.
 if (selection == null) {
             selection = StructuredSelection.EMPTY;
         }

         // According to the dictionary, a selection is "one that
 // is selected", or "a collection of selected things".
 // In reflection of this, we deal with one or a collection.

         // special case: structured selections
 if (selection instanceof IStructuredSelection) {
             return isEnabledFor((IStructuredSelection) selection);
         }

         // special case: text selections
 // Code should read
 // if (selection instanceof ITextSelection) {
 // int count = ((ITextSelection) selection).getLength();
 // return isEnabledFor(selection, count);
 // }
 // use Java reflection to avoid dependence of org.eclipse.jface.text
 // which is in an optional part of the generic workbench
 Class tselClass = getTextSelectionClass();
         if (tselClass != null && tselClass.isInstance(selection)) {
             try {
                 Method m = tselClass.getDeclaredMethod(
                         "getLength", new Class [0]); //$NON-NLS-1$
 Object r = m.invoke(selection, new Object [0]);
                 if (r instanceof Integer ) {
                     return isEnabledFor(selection, ((Integer ) r).intValue());
                 }
                 // should not happen - but enable if it does
 return true;
             } catch (NoSuchMethodException e) {
                 // should not happen - fall through if it does
 } catch (IllegalAccessException e) {
                 // should not happen - fall through if it does
 } catch (InvocationTargetException e) {
                 // should not happen - fall through if it does
 }
         }

         // all other cases
 return isEnabledFor(selection);
     }

     /**
      * Parses registry element to extract mode and selection elements that will
      * be used for verification.
      */
     private void parseClasses(IConfigurationElement config) {
         // Get enables for.
 String enablesFor = config
                 .getAttribute(IWorkbenchRegistryConstants.ATT_ENABLES_FOR);
         if (enablesFor == null) {
             enablesFor = "*"; //$NON-NLS-1$
 }
         if (enablesFor.equals("*")) { //$NON-NLS-1$
 mode = ANY_NUMBER;
         } else if (enablesFor.equals("?")) { //$NON-NLS-1$
 mode = NONE_OR_ONE;
         } else if (enablesFor.equals("!")) { //$NON-NLS-1$
 mode = NONE;
         } else if (enablesFor.equals("+")) { //$NON-NLS-1$
 mode = ONE_OR_MORE;
         } else if (enablesFor.equals("multiple") //$NON-NLS-1$
 || enablesFor.equals("2+")) { //$NON-NLS-1$
 mode = MULTIPLE;
         } else {
             try {
                 mode = Integer.parseInt(enablesFor);
             } catch (NumberFormatException e) {
                 mode = UNKNOWN;
             }
         }

         // Get enablement block.
 IConfigurationElement[] children = config
                 .getChildren(IWorkbenchRegistryConstants.TAG_ENABLEMENT);
         if (children.length > 0) {
             enablementExpression = new ActionExpression(children[0]);
             return;
         }

         // Get selection block.
 children = config
                 .getChildren(IWorkbenchRegistryConstants.TAG_SELECTION);
         if (children.length > 0) {
             classes = new ArrayList ();
             for (int i = 0; i < children.length; i++) {
                 IConfigurationElement sel = children[i];
                 String cname = sel
                         .getAttribute(IWorkbenchRegistryConstants.ATT_CLASS);
                 String name = sel
                         .getAttribute(IWorkbenchRegistryConstants.ATT_NAME);
                 SelectionClass sclass = new SelectionClass();
                 sclass.className = cname;
                 sclass.nameFilter = name;
                 classes.add(sclass);
             }
         }
     }

     /**
      * Verifies if the element is an instance of a class with a given class
      * name. If direct match fails, implementing interfaces will be tested, then
      * recursively all superclasses and their interfaces.
      */
     private boolean verifyClass(Object element, String className) {
         Class eclass = element.getClass();
         Class clazz = eclass;
         boolean match = false;
         while (clazz != null) {
             // test the class itself
 if (clazz.getName().equals(className)) {
                 match = true;
                 break;
             }
             // test all the interfaces it implements
 Class [] interfaces = clazz.getInterfaces();
             for (int i = 0; i < interfaces.length; i++) {
                 if (interfaces[i].getName().equals(className)) {
                     match = true;
                     break;
                 }
             }
             if (match == true) {
                 break;
             }
             // get the superclass
 clazz = clazz.getSuperclass();
         }
         return match;
     }

     /**
      * Verifies if the given element matches one of the selection requirements.
      * Element must at least pass the type test, and optionally wildcard name
      * match.
      */
     private boolean verifyElement(IAdaptable element) {
         if (classes.isEmpty()) {
             return true;
         }
         for (int i = 0; i < classes.size(); i++) {
             SelectionClass sc = (SelectionClass) classes.get(i);
             if (verifyClass(element, sc.className) == false) {
                 continue;
             }
             if (sc.nameFilter == null) {
                 return true;
             }
             IWorkbenchAdapter de = (IWorkbenchAdapter) Util.getAdapter(element, IWorkbenchAdapter.class);
             if ((de != null)
                     && verifyNameMatch(de.getLabel(element), sc.nameFilter)) {
                 return true;
             }
         }
         return false;
     }

     /**
      * Compare selection count with requirements.
      */
     private boolean verifySelectionCount(int count) {
         if (count > 0 && mode == NONE) {
             return false;
         }
         if (count == 0 && mode == ONE_OR_MORE) {
             return false;
         }
         if (count > 1 && mode == NONE_OR_ONE) {
             return false;
         }
         if (count < 2 && mode == MULTIPLE) {
             return false;
         }
         if (mode > 0 && count != mode) {
             return false;
         }
         return true;
     }
 }

